package com.simonbuckle.ant.tasks;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.Reader;
import java.io.Writer;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.DirectoryScanner;
import org.apache.tools.ant.Task;
import org.apache.tools.ant.types.FileSet;
import org.apache.tools.ant.types.LogLevel;
import org.apache.tools.ant.types.Mapper;
import org.mozilla.javascript.ErrorReporter;
import org.mozilla.javascript.EvaluatorException;

import com.yahoo.platform.yui.compressor.CssCompressor;
import com.yahoo.platform.yui.compressor.JavaScriptCompressor;

public class CompressTask extends Task {

	private final List<FileSet> filesets = new ArrayList<FileSet>();
	private Mapper mapper;
	private boolean failOnError = true;

	private int linebreak = -1;
	private boolean munge = true;
	private boolean preserveAllSemiColons = false;
	private boolean disableOptimizations = false;
	private boolean verbose = false;
	private String todir;
	private String type = "js"; // Support the --type argument, defaults to 'js'

	public void addFileset(FileSet fileset) {
		filesets.add(fileset);
	}

	public void addMapper(Mapper mapper) {
		this.mapper = mapper;
	}

	public void setDisableOptimizations(boolean disableOptimizations) {
		this.disableOptimizations = disableOptimizations;
	}

	public void setFailOnError(boolean failOnError) {
		this.failOnError = failOnError;
	}

	public void setType(String type) {
		this.type = type;
	}

	public void setLinebreak(int linebreak) {
		this.linebreak = linebreak;
	}

	public void setMunge(boolean munge) {
		this.munge = munge;
	}

	public void setPreserveAllSemiColons(boolean preserveAllSemiColons) {
		this.preserveAllSemiColons = preserveAllSemiColons;
	}

	public void setToDir(String todir) {
		this.todir = todir;
	}

	public void setVerbose(boolean verbose) {
		this.verbose = verbose;
	}

	private void validateRequired() throws BuildException {
		StringBuilder errorString = new StringBuilder();

		if (mapper == null) {
			errorString.append("Mapper property is required\n");
		}
		if (todir == null || "".equals(todir)) {
			errorString.append("Output directory is not specified\n");
		}
		if (!(type.equals("js") || type.equals("css") || type.equals("auto"))) {
			errorString.append("Unknown type specified: " + type + ". Accepted values are 'js', 'css' or 'auto'.\n");
		}

		if (errorString.length() > 0) {
			throw new BuildException(errorString.toString());
		}
	}

	@Override
	public void execute() throws BuildException {
		validateRequired();

		/* Choose the compressor implementation - validation ensures it's one of three values */
		CompressorType compressor = type.equals("js") ? jsCompress : type.equals("css") ? cssCompress : autoCompress;

		Iterator<FileSet> iter = filesets.listIterator();

		while (iter.hasNext()) {
			FileSet fileset = iter.next();
			DirectoryScanner scanner = fileset.getDirectoryScanner(getProject());
			File dir = scanner.getBasedir();

			String[] files = scanner.getIncludedFiles();
			for (String file : files) {
				String[] output = mapper.getImplementation().mapFileName(file);
				if (output != null) {
					try {
						compressor.compress(new File(dir, file), new File(todir, output[0]));

						float oldsz = new File(dir, file).length();
						float newsz = new File(todir, output[0]).length();
						log(String.format("Compressed: %s (%,.1f%%)", file, ((oldsz - newsz) / oldsz) * 100));
					} catch (IOException io) {
						log("Failed to compress file: " + file, LogLevel.ERR.getLevel());
						log(io, LogLevel.ERR.getLevel());
						if (failOnError) {
							throw new BuildException(io);
						}
					}
				}
			}
		}
	}

	/**
	 * A form of strategy pattern - each accepted type of compression maps to (or should map to) an implementation of this interface.
	 */
	private interface CompressorType {
		void compress(File source, File dest) throws IOException;
	}

	/**
	 * This is a 'special' compressor that simply tries to guess which of the other compressors to use, based on the source file's extension (defaults
	 * to the JS compressor).
	 */
	private final CompressorType autoCompress = new CompressorType() {
		@Override
		public void compress(File source, File dest) throws IOException {
			((source.getName().endsWith(".css")) ? cssCompress : jsCompress).compress(source, dest);
		}
	};

	private final CompressorType cssCompress = new CompressorType() {
		@Override
		public void compress(File source, File dest) throws IOException {
			Reader in = null;
			Writer out = null;

			try {
				in = new BufferedReader(new FileReader(source));
				CssCompressor compressor = new CssCompressor(in);
				out = new BufferedWriter(new FileWriter(dest));
				compressor.compress(out, linebreak);
			} finally {
				if (in != null) {
					in.close();
				}
				if (out != null) {
					out.close();
				}
			}
		}
	};

	private final CompressorType jsCompress = new CompressorType() {
		@Override
		public void compress(File source, File dest) throws IOException {
			Reader in = null;
			Writer out = null;

			try {
				in = new BufferedReader(new FileReader(source));
				JavaScriptCompressor compressor = new JavaScriptCompressor(in, errorReporter);

				out = new BufferedWriter(new FileWriter(dest));

				compressor.compress(out, linebreak, munge, verbose, preserveAllSemiColons, disableOptimizations);
			} finally {
				if (in != null) {
					in.close();
				}
				if (out != null) {
					out.close();
				}
			}
		}

		/**
		 * The error reporter used - nothing fancy. It was moved out of the inline context of the compress method to clarify the code a little.
		 */
		private final ErrorReporter errorReporter = new ErrorReporter() {
			@Override
			public void warning(String message, String sourceName, int line, String lineSource, int lineOffset) {
				if (verbose) {
					log("\n" + (line < 0 ? "" : line + ":" + lineOffset) + " " + message, LogLevel.WARN.getLevel());
				}
			}

			@Override
			public EvaluatorException runtimeError(String message, String sourceName, int line, String lineSource, int lineOffset) {
				error(message, sourceName, line, lineSource, lineOffset);
				throw new EvaluatorException(message);
			}

			@Override
			public void error(String message, String sourceName, int line, String lineSource, int lineOffset) {
				if (verbose) {
					log("\n" + (line < 0 ? "" : line + ":" + lineOffset) + " " + message, LogLevel.ERR.getLevel());
				}
			}
		};
	};
}
